fix: Recent sort precision + search UX overhaul + devcontainer DX#39
Merged
Conversation
…rt is correct
ListPages and Context stored each page's mtime via time.RFC3339, which
has only second-level precision. After a bulk operation (wiki sync git
pull, mass agent edit, etc.) many files share the same wall-clock
second on disk and therefore share an identical 'modified' string in
the index. The query ORDER BY modified DESC then has no tiebreaker
and SQLite returns rows in an arbitrary internal order — most user-
visibly: the 'Sort: Recent' list in the web UI did not actually reflect
recency.
- Write 'modified' with time.RFC3339Nano so the nanosecond precision
of the filesystem mtime survives into the index.
- Add 'path ASC' as a stable tiebreaker on the two queries that sort
by modified.
- Parse with RFC3339Nano too. (Existing seconds-precision values from
older databases parse fine — time.Parse handles the missing
fractional component.)
Old rows will get re-stored with nanosecond precision the next time
their files change; until then the path tiebreaker keeps ordering
stable.
… button
- Lazy-initialize searchQuery from localStorage ('mm-search-query') and
persist on change, so a filter survives a page reload.
- On first mount, restore the filtered list if a saved query is present;
otherwise load the full page list as before.
- Add a small × clear button rendered inside the search input when the
field is non-empty. Clicking it clears the query, removes the
localStorage entry (next render), and re-loads the full page list.
…able
- appPort: bind 127.0.0.1:51888->4242 explicitly instead of letting
Docker dual-stack the host loopback. VS Code's port panel was
suggesting http://[::1]:51888/ on dual-stack hosts; since the
container's server only listens on IPv4, that URL hung. We don't
need IPv6 for local dev, so drop it from the mapping entirely.
- launch.json: pass --addr 0.0.0.0:4242 to 'mind-map Server' and
'mind-map Server (no WebUI)'. The serve command defaults to
127.0.0.1:4242, which inside a container is unreachable from the
Docker port forward. Binding 0.0.0.0:4242 inside the container lets
the host's 127.0.0.1:51888 forward through. Outside the container
this is still local-only because the host mapping is 127.0.0.1.
- launch.json: WebUI debug config now opens http://localhost:51888 (the
forwarded host port), not :4242. Launched in the host's Chrome via
browserLaunchLocation 'ui', so it must use the host-side port.
When a page is opened with a non-empty search filter, every token of
the query is wrapped in <mark> in the rendered markdown, and the first
match is scrolled into view. Previously users had to scan long pages
manually to find where the FTS hit was.
- highlightHTML parses the marked-rendered HTML into a real DOM via
DOMParser, walks text nodes, and wraps matches. This means tags,
attributes, hrefs, mermaid placeholders, and code-block syntax are
never touched — only the visible text.
- Skips text inside <script>, <style>, and existing <mark> elements.
- Query is split on whitespace; each token is matched independently,
case-insensitive, with Unicode word boundaries.
- useMemo keyed on body/query/editing keeps re-highlighting off the
hot path during unrelated re-renders.
- Editing mode bypasses highlighting (textarea, no rendered body).
- Adds .markdown mark CSS using the existing --accent color so the
highlight blends with both light and dark themes.
Extracted the search tokenization into shared helpers (searchTokens, searchRegex) and added a tiny <Highlighted> Preact component that wraps text in <mark>s. Used it in: - sidebar page list (title + path of every item) - page header (current page title and the path subline) Together with the earlier body-content highlighting, every place that shows page text now reflects the active search filter. Also broadened the mark CSS from .markdown mark to a global mark rule so the sidebar/header highlights inherit the same style.
- searchTokens now recognizes "quoted phrases" as a single token.
The quoted text passes through verbatim to FTS5 (which natively
interprets "phrase" as a phrase match) and is highlighted as one
unit on the page. Bare unquoted words still tokenize individually
(current behavior).
- searchRegex collapses interior whitespace in phrase tokens to \s+
so 'MCP server' still highlights when the rendered text has a
newline or extra spaces between the words.
- Mermaid render effect now depends on renderedBodyHTML instead of
current. Previously, when searchQuery changed, the body's
.mermaid <div>s were replaced by Preact but mermaid.run() never
ran on the new nodes, leaving the diagram source as raw text on
the page. Adding renderedBodyHTML to the deps fires mermaid on
every body change, including search re-highlights.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A grab-bag of related fixes that came out of investigating the "Sort: Recent shows wrong order" bug, plus several search-UX improvements that surfaced while testing.
Wiki engine
Recent sort actually sorts by recency now (
01855b5)modifiedwas stored viatime.RFC3339(second precision). Wiki-syncgit pullwrites batches of files within the same wall-clock second, so all rows ended up with an identicalmodifiedstring →ORDER BY modified DESChad nothing to tiebreak on and SQLite returned them in arbitrary internal order. Most visibly: the "Sort: Recent" list in the web UI did not reflect recency.time.RFC3339Nanoon both write paths so the filesystem's nanosecond mtime survives into the index.path ASCas a stable tiebreaker on the two queries that sort bymodified.RFC3339Nano(backward-compatible with legacy seconds-only strings).Open():ReindexseesidxMtime != diskMtimeand re-stores every row at ns precision.TestListPagesRecentSortDistinguishesSameSecondMtimes.Web UI
Persistent search filter + clear button (
626f2be)searchQueryfromlocalStorage('mm-search-query')and persist on every keystroke, so filters survive a page reload.Search-match highlighting in the rendered page (
e4a371c)<mark>in the rendered markdown, and scroll the first match into view.DOMParser), not a regex over raw HTML, so tags / attributes / hrefs / mermaid placeholders are never touched.<script>/<style>/ existing<mark>. Code blocks still get highlighted (users searching for an identifier usually want to see it).useMemokeyed on[body, query, editing]keeps re-highlighting off the hot path.Highlight matches in sidebar + page header (
9176844)Same
<Highlighted>Preact component over a sharedsearchTokens/searchRegexpair, applied to:Global
mark { ... }CSS usingvar(--accent)blends with both themes.Quoted phrase search + mermaid re-render (
4e963c4)searchTokensnow recognizes"quoted phrases"as a single token. The quoted text passes through verbatim to FTS5 (which natively interprets"phrase"as a phrase match) and highlights as one unit on the page. Bare unquoted words still tokenize individually.\s+, so"MCP server"still highlights when the rendered text has a newline or extra spaces.renderedBodyHTMLinstead ofcurrent. Previously, whensearchQuerychanged, the body's.mermaiddivs were replaced by Preact butmermaid.run()never re-ran on the new nodes, leaving diagram source as raw text on the page.Devcontainer DX (
34a012a)The browser launched from VS Code's Ports panel was opening
http://[::1]:51888/and getting "can't be reached":appPort: bind127.0.0.1:51888->4242explicitly instead of letting Docker dual-stack the host loopback. VS Code's port panel preferred[::1]on dual-stack hosts; the container only listens on IPv4, so that URL hung.launch.json: pass--addr 0.0.0.0:4242to the dev-server launch configs. Theservedefault is127.0.0.1:4242, which inside a container is unreachable from the Docker port-forward.launch.json: WebUI debug config now openshttp://localhost:51888(the forwarded host port), not:4242. Launched in the host's Chrome viabrowserLaunchLocation: "ui", so it must use the host-side port.Backward compatibility
Open()(ns-precision mtimes get re-stored as Reindex notices the mismatch).